package org.test4j.mock.faking.modifier;

import org.test4j.mock.faking.FakeInvoker;
import org.test4j.mock.faking.meta.FakeMethod;
import org.test4j.mock.faking.meta.FakeStates;
import org.test4j.mock.faking.meta.MethodId;
import org.test4j.mock.faking.meta.TimesVerify;
import org.test4j.mock.faking.util.ClassLoad;

import java.io.File;
import java.io.FileInputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.List;
import java.util.Vector;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;

import static org.test4j.mock.faking.util.ClassLoad.isDuringClassLoading;

/**
 * 桥接方式调用 mock method
 *
 * @author darui.wu
 */
public final class BridgeFakeInvocation implements InvocationHandler {
    private static final Object[] EMPTY_ARGS = {};
    /**
     * 进入非mock代码块
     * o - 没有定义mock行为
     * o - inv.process(...)
     */
    public final static String Enter_Non_Mock_Block = "_Enter_Non_Mock_Block_";

    public static final String BridgeID = "$FakeInv";

    public static final InvocationHandler INSTANCE = new BridgeFakeInvocation();

    private BridgeFakeInvocation() {
    }

    /**
     * called by generate byte code
     * {@link FakeMethodModifier#callFakeMethod()}
     */
    @Override
    public Object invoke(Object target, Method method, Object[] args) {
        String classDesc = (String) args[0];
        if (notToBeMocked(target)) {
            return Enter_Non_Mock_Block;
        }
        String name = (String) args[1];
        String desc = (String) args[2];
        Class declaredClass = ClassLoad.loadClass(classDesc);
        Class realClass = target == null ? declaredClass : target.getClass();
        MethodId methodId = new MethodId(realClass, declaredClass, name, desc)
            .setTarget(realClass, target);
        TimesVerify.increaseTimes(methodId);
        FakeMethod fakeMethod = FakeStates.getLastMethod(methodId);
        if (fakeMethod == null || fakeMethod.isProceeding()) {
            return Enter_Non_Mock_Block;
        }
        /**
         * 被mock对象类(等于realClassDesc, 或其子类)
         */
        Object[] fakedArgs = extractArguments(3, args);
        FakeInvoker fakeInvoker = new FakeInvoker(target, fakeMethod, classDesc, name, desc, fakedArgs);
        return fakeInvoker.callFakeMethod();
    }

    public static Object[] extractArguments(int startingIndex, Object[] args) {
        if (args.length > startingIndex) {
            Object[] targetMemberArgs = new Object[args.length - startingIndex];
            System.arraycopy(args, startingIndex, targetMemberArgs, 0, targetMemberArgs.length);
            return targetMemberArgs;
        }
        return EMPTY_ARGS;
    }

    /**
     * 方法不可被mock
     *
     * @param instance
     * @return
     */
    protected boolean notToBeMocked(Object instance) {
        boolean duringClassLoading = isDuringClassLoading();
        if (!duringClassLoading) {
            return false;
        }
        Class aClass = instance.getClass();
        if (NOT_TO_BE_MOCKED.contains(aClass)) {
            return true;
        }
        for (Class notMocked : NOT_TO_BE_MOCKED_SUB) {
            if (notMocked.isAssignableFrom(aClass)) {
                return true;
            }
        }
        return false;
    }

    private static List<Class> NOT_TO_BE_MOCKED = Arrays.asList(
        File.class, URL.class, FileInputStream.class, Manifest.class
    );

    private static List<Class> NOT_TO_BE_MOCKED_SUB = Arrays.asList(
        JarFile.class, JarEntry.class, Vector.class, Hashtable.class
    );
}